package org.wu.framework.lazy.orm.database.lambda.stream.script;

import org.wu.framework.core.io.ScriptResource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.wu.framework.core.NormalUsedString;
import org.wu.framework.core.exception.AssertFactory;
import org.wu.framework.core.exception.RuntimeExceptionFactory;
import org.wu.framework.core.utils.StringUtils;

import javax.script.ScriptException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.LineNumberReader;
import java.sql.Connection;
import java.sql.SQLException;
import java.sql.SQLWarning;
import java.sql.Statement;
import java.util.ArrayList;
import java.util.List;

public class ScriptUtils {

    private static final Logger logger = LoggerFactory.getLogger(ScriptUtils.class);

    public static void executeSqlScript(Connection connection, ScriptResource resource,
                                        boolean ignoreFailedDrops, String[] commentPrefixes, String separator,
                                        String blockCommentStartDelimiter, String blockCommentEndDelimiter) throws ScriptException {

        try {
            if (logger.isDebugEnabled()) {
                logger.debug("Executing SQL script from " + resource);
            }
            long startTime = System.currentTimeMillis();

            String script = "";
            try {
                script = readScript(resource, commentPrefixes, blockCommentEndDelimiter);
            } catch (IOException ex) {
                RuntimeExceptionFactory.of(ex);
            }

            if (separator == null) {
                separator = NormalUsedString.SEMICOLON;
            }

            List<String> statements = new ArrayList<>();
            splitSqlScript(resource, script, separator, commentPrefixes, blockCommentStartDelimiter,
                    blockCommentEndDelimiter, statements);

            int stmtNumber = 0;
            Statement stmt = connection.createStatement();
            try {
                for (String statement : statements) {
                    stmtNumber++;
                    try {
                        stmt.execute(statement);
                        int rowsAffected = stmt.getUpdateCount();
                        if (logger.isDebugEnabled()) {
                            logger.debug(rowsAffected + " returned as update count for SQL: " + statement);
                            SQLWarning warningToLog = stmt.getWarnings();
                            while (warningToLog != null) {
                                logger.debug("SQLWarning ignored: SQL state '" + warningToLog.getSQLState() +
                                        "', error code '" + warningToLog.getErrorCode() +
                                        "', message [" + warningToLog.getMessage() + "]");
                                warningToLog = warningToLog.getNextWarning();
                            }
                        }
                    } catch (SQLException ex) {
                        ex.printStackTrace();
                        RuntimeExceptionFactory.of(statement, stmtNumber, resource, ex);
                    }
                }
            } finally {
                try {
                    stmt.close();
                } catch (Throwable ex) {
                    logger.trace("Could not connectionClose JDBC Statement", ex);
                }
            }

            long elapsedTime = System.currentTimeMillis() - startTime;
            if (logger.isDebugEnabled()) {
                logger.debug("Executed SQL script from " + resource + " in " + elapsedTime + " ms.");
            }
        } catch (Exception ex) {
            if (ex instanceof ScriptException scriptException) {
                throw scriptException;
            }
            RuntimeExceptionFactory.of(
                    "Failed to execute database script from resource [" + resource.getPath() + "]", ex);
        }
    }

    static String readScript(ScriptResource resource,
                             String[] commentPrefixes, String blockCommentEndDelimiter) throws IOException {

        try (LineNumberReader lnr = new LineNumberReader(new InputStreamReader(resource.getInputStream()))) {
            return readScript(lnr, commentPrefixes, blockCommentEndDelimiter);
        }
    }

    public static String readScript(LineNumberReader lineNumberReader, String[] commentPrefixes,
                                    String blockCommentEndDelimiter) throws IOException {

        String currentStatement = lineNumberReader.readLine();
        StringBuilder scriptBuilder = new StringBuilder();
        while (currentStatement != null) {
            if ((blockCommentEndDelimiter != null && currentStatement.contains(blockCommentEndDelimiter)) ||
                    (commentPrefixes != null && !startsWithAny(currentStatement, commentPrefixes, 0))) {
                if (!scriptBuilder.isEmpty()) {
                    scriptBuilder.append('\n');
                }
                scriptBuilder.append(currentStatement);
            }
            currentStatement = lineNumberReader.readLine();
        }
        return scriptBuilder.toString();
    }

    public static void splitSqlScript(ScriptResource resource, String script,
                                      String separator, String[] commentPrefixes, String blockCommentStartDelimiter,
                                      String blockCommentEndDelimiter, List<String> statements) throws ScriptException {

        AssertFactory.hasText(script, "'script' must not be null or empty");
        AssertFactory.notNull(separator, "'separator' must not be null");
        AssertFactory.notNull(commentPrefixes, "'commentPrefixes' must not be null or empty");
        for (String commentPrefix : commentPrefixes) {
            AssertFactory.hasText(commentPrefix, "'commentPrefixes' must not contain null or empty elements");
        }
        AssertFactory.hasText(blockCommentStartDelimiter, "'blockCommentStartDelimiter' must not be null or empty");
        AssertFactory.hasText(blockCommentEndDelimiter, "'blockCommentEndDelimiter' must not be null or empty");

        StringBuilder sb = new StringBuilder();
        boolean inSingleQuote = false;
        boolean inDoubleQuote = false;
        boolean inEscape = false;

        for (int i = 0; i < script.length(); i++) {
            char c = script.charAt(i);
            if (inEscape) {
                inEscape = false;
                sb.append(c);
                continue;
            }
            // MySQL style escapes
            if (c == '\\') {
                inEscape = true;
                sb.append(c);
                continue;
            }
            if (!inDoubleQuote && (c == '\'')) {
                inSingleQuote = !inSingleQuote;
            } else if (!inSingleQuote && (c == '"')) {
                inDoubleQuote = !inDoubleQuote;
            }
            if (!inSingleQuote && !inDoubleQuote) {
                if (script.startsWith(separator, i)) {
                    // We've reached the end of the current statement
                    if (!sb.isEmpty()) {
                        statements.add(sb.toString());
                        sb = new StringBuilder();
                    }
                    i += separator.length() - 1;
                    continue;
                } else if (startsWithAny(script, commentPrefixes, i)) {
                    // Skip over any content from the start of the comment to the EOL
                    int indexOfNextNewline = script.indexOf('\n', i);
                    if (indexOfNextNewline > i) {
                        i = indexOfNextNewline;
                        continue;
                    } else {
                        // If there's no EOL, we must be at the end of the script, so stop here.
                        break;
                    }
                } else if (script.startsWith(blockCommentStartDelimiter, i)) {
                    // Skip over any block comments
                    int indexOfCommentEnd = script.indexOf(blockCommentEndDelimiter, i);
                    if (indexOfCommentEnd > i) {
                        i = indexOfCommentEnd + blockCommentEndDelimiter.length() - 1;
                        continue;
                    } else {
                        RuntimeExceptionFactory.of(
                                "Missing block comment end delimiter: " + blockCommentEndDelimiter + resource);
                    }
                } else if (c == ' ' || c == '\r' || c == '\n' || c == '\t') {
                    // Avoid multiple adjacent whitespace characters
                    if (!sb.isEmpty() && sb.charAt(sb.length() - 1) != ' ') {
                        c = ' ';
                    } else {
                        continue;
                    }
                }
            }
            sb.append(c);
        }

        if (StringUtils.hasText(sb)) {
            statements.add(sb.toString());
        }
    }

    private static boolean startsWithAny(String script, String[] prefixes, int offset) {
        for (String prefix : prefixes) {
            if (script.startsWith(prefix, offset)) {
                return true;
            }
        }
        return false;
    }
}
